/*              Copyright(c) 2008 Dave Sexton              *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.Xml.XPath;
using System.ComponentModel;
using Microsoft.Ddue.Tools;

namespace DaveSexton.Sandcastle
{
	[CLSCompliant(false)]
	[DisplayName("Resolve external links")]
	[Description("Resolves <see xref='id'/>, <see vref='id_or_url'/> and <seealso/> likewise, to URLs specified in the component's mappings.")]
	public sealed partial class ResolveExternalLinksComponent : BuildComponent
	{
		private delegate Uri GetHrefStrategy(string id, UrlMappingReference map, ResolveExternalLinksComponentConfiguration configuration);
		private delegate void ResolveExternalLinksContinuation(XPathNavigator navigator, string id, UrlMappingReference map, ResolveExternalLinksComponentConfiguration configuration, Uri uri);
		
		/// <summary>
		/// Executes the component's primary function against the specified <paramref name="document"/>.
		/// </summary>
		/// <param name="document">The <see cref="XmlDocument"/> that contains the current topic on which to 
		/// execute the component.</param>
		/// <param name="topicId">The id of the topic to which the <paramref name="document"/> applies.</param>
		/// <param name="configuration">Configuration data for the component that contains its outer XML 
		/// from the Build Assembler configuration file.  The data is initialized when the component is 
		/// constructed.</param>
		private void Apply(XmlDocument document, string topicId, ResolveExternalLinksComponentConfiguration configuration)
		{
			if (configuration.Enabled)
			{
				XmlNamespaceManager resolver = configuration.NamespaceManager;

				ResolveExternalLinks(document, topicId, configuration, XPathExpression.Compile(configuration.SeeXPath, resolver), ReplaceWithAnchor);

				if (configuration.ReplaceSeeAlsoLinks)
					ResolveExternalLinks(document, topicId, configuration, XPathExpression.Compile(configuration.SeeAlsoXPath, resolver), ReplaceAttributes);
			}
		}

		private void ResolveExternalLinks(XmlDocument document, string topicId, ResolveExternalLinksComponentConfiguration configuration,
			XPathExpression select, ResolveExternalLinksContinuation forEach)
		{
			foreach (XPathNavigator navigator in BuildComponentUtilities.ConvertNodeIteratorToArray(
				(XPathNodeIterator) document.CreateNavigator().Evaluate(select)))
			{
				UrlMappingReference map;
				GetHrefStrategy getHRefStrategy;

				string id = navigator.GetAttribute("xref", "");

				if (string.IsNullOrEmpty(id))
				{
					id = navigator.GetAttribute("vref", "");

					map = configuration.GetMapping(topicId, id);
					getHRefStrategy = GetVRef;
				}
				else
				{
					map = configuration.GetMapping(topicId, id);
					getHRefStrategy = GetXRef;
				}

				Uri uri = getHRefStrategy(id, map, configuration);

				forEach(navigator, id, map, configuration, uri);
			}
		}

		private void ReplaceAttributes(XPathNavigator navigator, string id, UrlMappingReference map, ResolveExternalLinksComponentConfiguration configuration, Uri uri)
		{
			if (uri == null)
				navigator.DeleteSelf();
			else
			{
				while (navigator.MoveToFirstAttribute())
					navigator.DeleteSelf();

				navigator.CreateAttribute(null, "href", null, uri.ToString());

				string target;

				if (map == null)
					target = configuration.Target;
				else
				{
					target = map.Group.Target;

					if (string.IsNullOrEmpty(target))
						target = configuration.Target;
				}

				if (!string.IsNullOrEmpty(target))
					navigator.CreateAttribute(null, "target", null, target);
			}
		}

		private void ReplaceWithAnchor(XPathNavigator navigator, string id, UrlMappingReference map, ResolveExternalLinksComponentConfiguration configuration, Uri uri)
		{
			using (XmlWriter writer = navigator.InsertAfter())
			{
				if (uri == null)
				{
					writer.WriteStartElement("span");
					writer.WriteAttributeString("class", "nolink");

					if (navigator.HasChildren)
						navigator.WriteSubtree(writer);
					else
						writer.WriteString(id);

					writer.WriteEndElement();
				}
				else
				{
					string target;

					if (map == null)
						target = configuration.Target;
					else
					{
						target = map.Group.Target;

						if (string.IsNullOrEmpty(target))
							target = configuration.Target;
					}

					string xmlFormat = configuration.SeeLinkXml;

					if (string.IsNullOrEmpty(xmlFormat))
					{
						writer.WriteStartElement("a");
						writer.WriteAttributeString("href", uri.ToString());

						if (!string.IsNullOrEmpty(target))
							writer.WriteAttributeString("target", target);

						writer.WriteString((navigator.HasChildren) ? navigator.InnerXml : id);
						writer.WriteEndElement();
					}
					else
					{
						string xml = string.Format(System.Globalization.CultureInfo.InvariantCulture,
							xmlFormat, uri, target, (navigator.HasChildren) ? navigator.InnerXml : id);

						XmlDocument document = new XmlDocument();
						document.LoadXml("<root>" + xml + "</root>");

						// NOTE: WriteRaw is not supposed to escape XML, however it does when used here.  The workaround is to load the XML into a document and use WriteNode instead.
						writer.WriteNode(document.DocumentElement.CreateNavigator(), false);
					}
				}
			}

			navigator.DeleteSelf();
		}

		private Uri GetXRef(string id, UrlMappingReference map, ResolveExternalLinksComponentConfiguration configuration)
		{
			if (map == null)
			{
				WriteMessage(MessageLevel.Warn, "No mapping found for xref ID: " + id);
				return null;
			}

			UrlMappingGroup group = map.Group;
			Uri uri = map.Mapping.Href;

			if (uri == null)
			{
				if (group.BaseUri != null)
					return group.BaseUri;
				else
				{
					string baseUri = configuration.BaseUri;

					if (string.IsNullOrEmpty(baseUri))
					{
						WriteMessage(MessageLevel.Warn, "No base or URL found for xref ID: " + id);
						return null;
					}
					else
						return new Uri(baseUri, UriKind.RelativeOrAbsolute);
				}
			}
			else
				return PrependBaseUri(uri, group, configuration);
		}

		private Uri GetVRef(string id, UrlMappingReference map, ResolveExternalLinksComponentConfiguration configuration)
		{
			if (map == null)
			// assume that id is a relative path
			{
				string baseUri = configuration.BaseUri;

				if (string.IsNullOrEmpty(baseUri))
				{
					WriteMessage(MessageLevel.Warn, "No base or mapping found for vref ID: " + id);
					return new Uri(id, UriKind.RelativeOrAbsolute);
				}
				else
					return new Uri(new Uri(baseUri, UriKind.RelativeOrAbsolute), id);
			}

			UrlMappingGroup group = map.Group;
			Uri uri = map.Mapping.Href;

			if (uri == null)
				// assume that id is a relative path
				uri = new Uri(id, UriKind.RelativeOrAbsolute);
			
			return PrependBaseUri(uri, group, configuration);
		}

		private static Uri PrependBaseUri(Uri uri, UrlMappingGroup group, ResolveExternalLinksComponentConfiguration configuration)
		{
			if (uri.IsAbsoluteUri)
				return uri;
			else if (group.BaseUri != null)
				return new Uri(group.BaseUri, uri);
			else
			{
				string baseUri = configuration.BaseUri;

				if (string.IsNullOrEmpty(baseUri))
					return uri;
				else
					return new Uri(new Uri(baseUri, UriKind.RelativeOrAbsolute), uri);
			}
		}
	}
}
